﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.IO;

namespace JSMerge {

	public class Project {

		private string m_OutputFile = null;
		private string m_RootDirectory = ".\\";
		private string m_OutputHeader = null;
		private string m_OutputFooter = null;
		private bool m_ProcessTags = true;
		private ProjectSettings m_Settings = null;
		private List<FileSetting> m_Files = null;
		private List<PathSetting> m_IncludePaths = null;
		private List<PathSetting> m_IgnorePaths = null;

		private List<string> fileNameSpaces = new List<string>();

		#region " Properties "

		public string OutputFile {
			get { return m_OutputFile; }
			set { m_OutputFile = value; }
		}

		public string OutputHeader {
			get { return m_OutputHeader; }
			set { m_OutputHeader = value; }
		}

		public string OutputFooter {
			get { return m_OutputFooter; }
			set { m_OutputFooter = value; }
		}

		public bool ProcessTags {
			get { return m_ProcessTags; }
			set { m_ProcessTags = value; }
		}

		public string RootDirectory {
			get { return m_RootDirectory; }
			set { m_RootDirectory = value; }
		}

		public ProjectSettings Settings {
			get { return m_Settings; }
			set { m_Settings = value; }
		}

		public FileSetting[] Files {
			get { return m_Files.ToArray(); }
		}

		public PathSetting[] IncludePaths {
			get { return m_IncludePaths.ToArray(); }
		}

		public PathSetting[] IgnorePaths {
			get { return m_IgnorePaths.ToArray(); }
		}

		#endregion

		#region " Constructor(s) "

		public Project() {
			this.Settings = new ProjectSettings();
			m_Files = new List<FileSetting>();
			m_IncludePaths = new List<PathSetting>();
			m_IgnorePaths = new List<PathSetting>();
		}

		#endregion

		#region " Methods "

		#region " Merging "

		/// <summary>
		/// Combines the files
		/// </summary>
		public void CombineFiles() {
			ReOrderFiles();
			//merge the files
			m_OutputFile = Path.GetFullPath( m_OutputFile );
			if ( File.Exists( m_OutputFile ) ) {
				File.Delete( m_OutputFile );
			}
			using ( FileStream fs = new FileStream( m_OutputFile, FileMode.OpenOrCreate, System.Security.AccessControl.FileSystemRights.FullControl, FileShare.ReadWrite | FileShare.Delete, 4096, FileOptions.None ) ) {
				byte[] newLine = Encoding.UTF8.GetBytes( "\r\n" );
				if ( m_OutputHeader != null ) {
					byte[] header = Encoding.UTF8.GetBytes( m_OutputHeader );
					fs.Write( header, 0, header.Length );
					fs.Write( newLine, 0, newLine.Length );
					fs.Write( newLine, 0, newLine.Length );
				}
				if ( fileNameSpaces != null && fileNameSpaces.Count > 0 ) {
					for ( int x = 0; x < fileNameSpaces.Count; x++ ) {
						byte[] ns = Encoding.UTF8.GetBytes( fileNameSpaces[x] );
						fs.Write( ns, 0, ns.Length );
						fs.Write( newLine, 0, newLine.Length );
					}
				}
				for ( int x = 0; x < m_Files.Count; x++ ) {
					if ( m_Files[x].FileName.Equals( m_OutputFile, StringComparison.InvariantCultureIgnoreCase ) == false ) {
						Console.WriteLine( "Merging File: {0}", m_Files[x].FileName );
						bool newLines = ( ( m_Settings.NewLine.HasValue && m_Settings.NewLine.Value ) || ( m_Settings.NewLine.HasValue == false && m_Files[x].NewLine ) );
						bool stripComments = ( ( m_Settings.StripComments.HasValue && m_Settings.StripComments.Value ) || ( m_Settings.StripComments.HasValue == false && m_Files[x].StripComments ) );
						bool stripReferences = ( ( m_Settings.StripReferences.HasValue && m_Settings.StripReferences.Value ) || ( m_Settings.StripReferences.HasValue == false && m_Files[x].StripReferences ) );
						using ( var sr = new StreamReader( m_Files[x].FileName ) ) {
							string line = null;
							while ( ( line = sr.ReadLine() ) != null ) {
								bool mergeLine = true;
								if ( m_ProcessTags && ShouldRemoveLine( line ) ) {
									mergeLine = false;
								}
								if ( stripReferences ) {
									int commentIndex = line.IndexOf( "//" );
									int index = line.IndexOf( String.Format( "<{0} ", Properties.Settings.Default.VisualStudioReferenceTag ) );
									if ( commentIndex > -1 && commentIndex < index ) {
										if ( commentIndex > 0 ) {
											line = line.Substring( 0, commentIndex );
										} else {
											line = null;
										}
									}
								}
								if ( stripComments ) {
									line = StringUtilities.StripComments( line );
								}
								if ( line != null && mergeLine && ( m_Settings.StripBlankLines == false || String.IsNullOrWhiteSpace( line ) == false ) ) {
									byte[] lineBytes = Encoding.UTF8.GetBytes( line );
									fs.Write( lineBytes, 0, lineBytes.Length );
									fs.Write( newLine, 0, newLine.Length );
								}
							}
							if ( newLines ) {
								fs.Write( newLine, 0, newLine.Length );
							}
							fs.Flush();
						}
					}
				}
				if ( m_OutputFooter != null ) {
					byte[] footer = Encoding.UTF8.GetBytes( m_OutputFooter );
					fs.Write( newLine, 0, newLine.Length );
					fs.Write( footer, 0, footer.Length );
				}
				fs.Close();
			}
		}

		private bool ShouldRemoveLine( string line ) {
			bool result = false;
			string comment = StringUtilities.ExtractComment( line );
			if ( String.IsNullOrEmpty( comment ) == false ) {
				int tagStartIndex = comment.IndexOf( '<' );
				int tagEndIndex = comment.IndexOf( '>' );
				if ( tagStartIndex > -1 && tagEndIndex > -1 && tagStartIndex < tagEndIndex ) {
					string tag = comment.Substring( tagStartIndex, tagEndIndex - tagStartIndex + 1 );
					if ( StringUtilities.IsJSMergeTag( tag ) ) {
						XElement element = XElement.Parse( tag );
						if ( element.HasAttributes ) {
							if ( element.Attribute( "moveTo" ) != null || element.Attribute( "moveBy" ) != null ) {
								result = true;
							} else {
								if ( element.Attribute( "namespace" ) != null ) {
									if ( Convert.ToBoolean( element.Attribute( "namespace" ).Value ) ) {
										result = true;
									}
								}
								if ( m_Settings.ProcessRemoveTags && element.Attribute( "remove" ) != null ) {
									if ( Convert.ToBoolean( element.Attribute( "remove" ).Value ) ) {
										result = true;
									}
								}
							}
						}
					}
				}
			}
			return result;
		}

		/// <summary>
		/// Orders the files based on their settings.
		/// </summary>
		private void ReOrderFiles() {
			List<FileSetting> unorderedFiles = new List<FileSetting>( m_Files );
			for ( int x = 0; x < unorderedFiles.Count; x++ ) {
				int index = m_Files.IndexOf( unorderedFiles[x] );
				Console.Write( ( x.ToString() + '.' ).PadRight( 5 ) + "File '{0}'", m_Files[index].FileName );
				int? moveIndex = null;
				bool swap = false;
				if ( m_Files[index].MoveTo.HasValue ) {
					moveIndex = m_Files[index].MoveTo.Value;
					swap = true;
				} else if ( m_Files[index].MoveBy != 0 ) {
					moveIndex = x + m_Files[index].MoveBy;
				}
				if ( moveIndex.HasValue ) {
					Console.WriteLine( " - Moving File to new Index {0}.", moveIndex.Value );
					if ( moveIndex.Value < 0 )
						moveIndex = 0;
					else if ( moveIndex.Value >= m_Files.Count )
						moveIndex = m_Files.Count - 1;
					if ( swap ) {
						FileSetting temp = m_Files[moveIndex.Value];
						m_Files[moveIndex.Value] = m_Files[index];
						m_Files[index] = temp;
					} else {
						FileSetting temp = m_Files[index];
						m_Files.RemoveAt( index );
						m_Files.Insert( moveIndex.Value - 1, temp );
					}
				} else {
					Console.WriteLine('.');
				}
			}
		}

		#endregion

		#region " Preprocessing "

		/// <summary>
		/// Generates file settings for every file found in the include paths, and skipping any 
		/// files in ignore paths.
		/// </summary>
		public int ProcessPaths() {
			Directory.SetCurrentDirectory( m_RootDirectory );
			List<FileSetting> files = new List<FileSetting>();
			fileNameSpaces.Clear();
			//get all files to be included
			if ( m_IncludePaths != null ) {
				string[] includeFileNames = GetFiles( m_IncludePaths );
				if ( includeFileNames != null ) {
					for ( int x = 0; x < includeFileNames.Length; x++ ) {
						files.Add( new FileSetting( Path.GetFullPath( includeFileNames[x] ) ) );
					}
				}
			}
			if ( m_IgnorePaths != null ) {
				string[] ignoreFiles = GetFiles( m_IgnorePaths );
				for ( int x = files.Count - 1; x >= 0; x-- ) {
					string fileName = files[x].FileName;
					if ( ignoreFiles.Any( a => Path.GetFullPath( a ).Equals( fileName, StringComparison.InvariantCultureIgnoreCase ) ) ) {
						files.RemoveAt( x );
					}
				}
			}
			//remove duplicates
			for ( int x = files.Count - 1; x >= 0; x-- ) {
				var existing = files.FirstOrDefault( a => a.FileName.Equals( files[x].FileName, StringComparison.InvariantCultureIgnoreCase ) );
				if ( existing != files[x] ) {
					files.RemoveAt( x );
				}
			}
			if ( this.ProcessTags ) {
				//extract file settings
				for ( int x = 0; x < files.Count; x++ ) {
					using ( var sr = new StreamReader( files[x].FileName ) ) {
						string line = null;
						while ( ( line = sr.ReadLine() ) != null ) {
							bool isNS = false;
							ParseFileMergeElement( files[x], line, out isNS );
							if ( isNS ) {
								string ns = StringUtilities.StripComments( line );
								if ( String.IsNullOrWhiteSpace( ns ) == false ) {
									ns = ns.Trim( new char[] { ' ', '\t', '\r', '\n' } );
									if ( fileNameSpaces.Any( a => a.Equals( ns, StringComparison.InvariantCultureIgnoreCase ) ) == false ) {
										fileNameSpaces.Add( ns );
									}
								}
							}
						}
					}
				}
			}
			m_Files = files;
			//remove files to be ignored
			return files.Count;
		}

		/// <summary>
		/// Parses all file attributes from a given line (if any), and updates the settings.
		/// </summary>
		private bool ParseFileMergeElement( FileSetting setting, string line, out bool isNamespace ) {
			bool foundFileAttributes = false;
			string comment = StringUtilities.ExtractComment( line );
			isNamespace = false;
			if ( String.IsNullOrEmpty( comment ) == false ) {
				int tagStartIndex = comment.IndexOf( '<' );
				int tagEndIndex = comment.IndexOf( '>' );
				if ( tagStartIndex > -1 && tagEndIndex > -1 && tagStartIndex < tagEndIndex ) {
					string tag = comment.Substring( tagStartIndex, tagEndIndex - tagStartIndex + 1 );
					if ( StringUtilities.IsJSMergeTag( tag ) ) {
						XElement element = XElement.Parse( tag );
						if ( element.HasAttributes ) {
							//process file attributes
							if ( element.Attribute( "moveBy" ) != null ) {
								int moveby = 0;
								if ( int.TryParse( element.Attribute( "moveBy" ).Value, out moveby ) ) {
									setting.MoveBy = moveby;
									foundFileAttributes = true;
								}
							}
							if ( element.Attribute( "moveTo" ) != null ) {
								int moveto = 0;
								if ( int.TryParse( element.Attribute( "moveTo" ).Value, out moveto ) ) {
									setting.MoveTo = moveto;
									foundFileAttributes = true;
								} else {
									setting.MoveTo = null;
								}
							}
							if ( element.Attribute( "newLine" ) != null ) {
								setting.NewLine = Convert.ToBoolean( element.Attribute( "newLine" ).Value );
							}
							if ( element.Attribute( "stripComments" ) != null ) {
								setting.StripComments = Convert.ToBoolean( element.Attribute( "stripComments" ).Value );
							}
							if ( element.Attribute( "stripReferences" ) != null ) {
								setting.StripReferences = Convert.ToBoolean( element.Attribute( "stripReferences" ).Value );
							}
							if ( element.Attribute( "namespace" ) != null ) {
								if ( Convert.ToBoolean( element.Attribute( "namespace" ).Value ) ) {
									isNamespace = true;
								}
							}
						}
					}
				}
			}
			return foundFileAttributes;
		}

		/// <summary>
		/// Get all files as described by the given path settings.
		/// </summary>
		private string[] GetFiles( IEnumerable<PathSetting> paths ) {
			List<string> fileNames = new List<string>();
			if ( paths != null ) {
				foreach ( PathSetting path in paths ) {
					FileAttributes attr = File.GetAttributes( path.Path );
					if ( ( attr & FileAttributes.Directory ) == FileAttributes.Directory ) {
						if ( path.Recursive ) {
							fileNames.AddRange( Directory.GetFiles( path.Path, "*.js", SearchOption.AllDirectories ) );
						} else {
							string[] dirFiles = Directory.GetFiles( path.Path, "*.js", SearchOption.TopDirectoryOnly );
							if ( dirFiles != null ) {
								fileNames.AddRange( dirFiles );
							}
						}
					} else {
						fileNames.Add( path.Path );
					}
				}
			}
			return fileNames.ToArray();
		}

		#endregion

		#endregion

		#region " Static Methods "

		/// <summary>
		/// Builds a project object from the given combine file.
		/// </summary>
		public static Project FromFile( string projectFile ) {
			if ( projectFile == null ) {
				throw new ArgumentNullException( "projectFile", "The project file name must be specified." );
			}
			if ( File.Exists( projectFile ) == false ) {
				throw new FileNotFoundException( "The specified project file was not found.", projectFile );
			}
			if ( Path.GetExtension( projectFile ) != ".jsm" ) {
				throw new IOException( "Invalid file extension.\nJSMerge files must end in with the '.jsm' extension." );
			}
			XDocument xdoc = XDocument.Load( projectFile );
			Project project = FromXDocument( xdoc );
			if ( String.IsNullOrWhiteSpace( project.RootDirectory )
				|| project.RootDirectory == "."
				|| project.RootDirectory == ".\\"
				|| project.RootDirectory == ".\\\\" ) {
				project.RootDirectory = Path.GetDirectoryName( Path.GetFullPath( projectFile ) );
			}
			return project;
		}

		/// <summary>
		/// Builds a project object from a given xml document.
		/// </summary>
		private static Project FromXDocument( XDocument projectDoc ) {
			if ( projectDoc.Root != null && projectDoc.Root.Name == "JSMerge" ) {
				Project project = new Project();
				if ( projectDoc.Root.HasElements ) {
					if ( projectDoc.Root.Element( "Output" ) != null ) {
						project.OutputFile = Convert.ToString( projectDoc.Root.Element( "Output" ).Value );
					}
					if ( String.IsNullOrWhiteSpace( project.OutputFile ) ) {
						project.OutputFile = "Merged.js";
					} else if ( String.IsNullOrEmpty( Path.GetExtension( project.OutputFile ) ) ) {
						project.OutputFile += ".js";
					}
					if ( projectDoc.Root.Element( "OutputHeader" ) != null ) {
						project.OutputHeader = Convert.ToString( projectDoc.Root.Element( "OutputHeader" ).Value );
					}
					if ( projectDoc.Root.Element( "OutputFooter" ) != null ) {
						project.OutputFooter = Convert.ToString( projectDoc.Root.Element( "OutputFooter" ).Value );
					}
					if ( projectDoc.Root.Element( "RootDirectory" ) != null ) {
						project.RootDirectory = Convert.ToString( projectDoc.Root.Element( "RootDirectory" ).Value );
					}
					//get the settings
					if ( projectDoc.Root.Element( "NewLine" ) != null ) {
						project.Settings.NewLine = Convert.ToBoolean( projectDoc.Root.Element( "NewLine" ).Value );
					}
					if ( projectDoc.Root.Element( "StripComments" ) != null ) {
						project.Settings.StripComments = Convert.ToBoolean( projectDoc.Root.Element( "StripComments" ).Value );
					}
					if ( projectDoc.Root.Element( "StripReferences" ) != null ) {
						project.Settings.StripReferences = Convert.ToBoolean( projectDoc.Root.Element( "StripReferences" ).Value );
					}
					if ( projectDoc.Root.Element( "StripBlankLines" ) != null ) {
						project.Settings.StripBlankLines = Convert.ToBoolean( projectDoc.Root.Element( "StripBlankLines" ).Value );
					}
					if ( projectDoc.Root.Element( "ProcessRemove" ) != null ) {
						project.Settings.ProcessRemoveTags = Convert.ToBoolean( projectDoc.Root.Element( "ProcessRemove" ).Value );
					}
					//get the include paths
					foreach ( var path in from n in projectDoc.Root.Elements( "IncludePaths" )
										  from z in n.Descendants( "Path" )
										  where String.IsNullOrWhiteSpace( z.Value ) == false
										  select z ) {
						project.m_IncludePaths.Add( ExtractPath( path ) );
					}
					//get the ignore paths
					foreach ( var path in from n in projectDoc.Root.Elements( "IgnorePaths" )
										  from z in n.Descendants( "Path" )
										  where String.IsNullOrWhiteSpace( z.Value ) == false
										  select z ) {
						project.m_IgnorePaths.Add( ExtractPath( path ) );
					}
				}
				return project;
			}
			throw new ApplicationException( "Invalid JSMerge project file XML." );
		}

		/// <summary>
		/// Gets a path setting object from the xml element
		/// </summary>
		private static PathSetting ExtractPath( XElement pathElement ) {
			PathSetting pSetting = new PathSetting();
			pSetting.Path = pathElement.Value;
			if ( pathElement.Attribute( "recursive" ) != null ) {
				pSetting.Recursive = Convert.ToBoolean( pathElement.Attribute( "recursive" ).Value );
			}
			return pSetting;
		}

		#endregion

	}

}
